package com.youthburin.base.springjpa;

import static org.springframework.data.jpa.repository.query.QueryUtils.*;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import com.youthburin.base.util.DwzCondition;
import com.youthburin.base.util.DwzPagination;

public class MyRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements
		MyRepository<T, ID> {
	private static final Logger logger = LoggerFactory.getLogger(MyRepositoryImpl.class);

	private final EntityManager entityManager;
	private final Class<T> domainClass;

	public MyRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
		super(domainClass, entityManager);
		this.domainClass = domainClass;
		this.entityManager = entityManager;
	}

	@Override
	public T update(T t) {
		return entityManager.merge(t);
	}

	@Override
	public void dwzFindAll(DwzPagination dwzPagination) {
		Pageable pageable = dwzPagination.exportSpringDataJpaPageable();
		List<DwzCondition> conditionList = dwzPagination.getConditionList();
		CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		CriteriaQuery<T> c = cb.createQuery(domainClass);

		Root<T> from = c.from(domainClass);
		c.select(from);

		Sort sort = pageable.getSort();
		if (sort != null) {
			c.orderBy(toOrders(sort, from, cb));
		}

		if (CollectionUtils.isNotEmpty(conditionList)) {
			List<Predicate> criteria = new ArrayList<Predicate>();
			for (DwzCondition condition : conditionList) {
				Predicate predicate = getPredicate(cb, from, condition);
				if (predicate != null)
					criteria.add(predicate);
			}

			if (criteria.size() == 1) {
				c.where(criteria.get(0));
			} else if (criteria.size() > 1) {
				c.where(criteria.toArray(new Predicate[0]));
			}

		}
		TypedQuery<T> q = entityManager.createQuery(c);
		if (CollectionUtils.isNotEmpty(conditionList)) {
			for (DwzCondition condition : conditionList) {
				setParameter(q, condition);
			}
		}
		q.setFirstResult(pageable.getOffset());
		q.setMaxResults(pageable.getPageSize());
		Page<T> page = new PageImpl<T>(q.getResultList(), pageable, getTotalCount(conditionList));

		dwzPagination.importPageable(page);
	}

	private long getTotalCount(List<DwzCondition> conditionList) {
		CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		CriteriaQuery<Long> c = cb.createQuery(Long.class);

		Root<T> from = c.from(domainClass);
		c.select(cb.count(from));

		if (CollectionUtils.isNotEmpty(conditionList)) {
			List<Predicate> criteria = new ArrayList<Predicate>();
			for (DwzCondition condition : conditionList) {
				Predicate predicate = getPredicate(cb, from, condition);
				if (predicate != null)
					criteria.add(predicate);
			}

			if (criteria.size() == 1) {
				c.where(criteria.get(0));
			} else if (criteria.size() > 1) {
				c.where(criteria.toArray(new Predicate[0]));
			}
		}
		TypedQuery<Long> q = entityManager.createQuery(c);
		if (CollectionUtils.isNotEmpty(conditionList)) {
			for (DwzCondition condition : conditionList) {
				setParameter(q, condition);
			}
		}

		return q.getSingleResult();
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void setParameter(TypedQuery q, DwzCondition condition) {
		String key = condition.getName();
		String value = condition.getValue();
		DwzCondition.Operation[] noParameterOps = new DwzCondition.Operation[] { DwzCondition.Operation.CONTAINS,
				DwzCondition.Operation.STARTSWITH, DwzCondition.Operation.ENDSWITH, DwzCondition.Operation.LESS,
				DwzCondition.Operation.LESSOREQUALS, DwzCondition.Operation.GREATER,
				DwzCondition.Operation.GREATEROREQUALS };

		Class fieldClass = getFieldType(key);

		if (fieldClass != null && StringUtils.isNotBlank(value)
				&& !ArrayUtils.contains(noParameterOps, condition.getOp())) {
			if (fieldClass.isPrimitive()) {
				q.setParameter(key, getPrimitiveObject(fieldClass, value));
			} else {
				Object valueObject = null;
				if (Number.class.isAssignableFrom(fieldClass)) {
					valueObject = NumberUtils.createNumber(value);
				} else if (fieldClass.isAssignableFrom(String.class)) {
					valueObject = value;
				} else if (fieldClass.isAssignableFrom(Boolean.class)) {
					valueObject = Boolean.parseBoolean(value);
				}
				if (valueObject != null) {
					q.setParameter(key, valueObject);
				}
			}
		}
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	private Predicate getPredicate(CriteriaBuilder cb, Root<T> from, DwzCondition condition) {
		Predicate predicate = null;
		String key = condition.getName();
		String value = condition.getValue();

		Class fieldTypeClass = getFieldType(key);
		if (fieldTypeClass != null && StringUtils.isNotBlank(value)) {
			if (DwzCondition.Operation.EQUALS.equals(condition.getOp())) {
				predicate = cb.equal(from.get(key), cb.parameter(fieldTypeClass, key));
			} else if (DwzCondition.Operation.NOTEQUALS.equals(condition.getOp())) {
				predicate = cb.notEqual(from.get(key), cb.parameter(fieldTypeClass, key));
			} else if (DwzCondition.Operation.LESS.equals(condition.getOp())) {
				try {
					Number number = NumberUtils.createNumber(value);
					predicate = cb.lt(from.<Number> get(key), number);
				} catch (NumberFormatException e) {
					logger.warn("LESS操作，条件参数：" + key + "要求为数字，但实际不是数字![" + value + "]");
				}
			} else if (DwzCondition.Operation.LESSOREQUALS.equals(condition.getOp())) {
				try {
					Number number = NumberUtils.createNumber(value);
					predicate = cb.le(from.<Number> get(key), number);
				} catch (NumberFormatException e) {
					logger.warn("LESSOREQUALS操作，条件参数：" + key + "要求为数字，但实际不是数字![" + value + "]");
				}
			} else if (DwzCondition.Operation.GREATER.equals(condition.getOp())) {
				try {
					Number number = NumberUtils.createNumber(value);
					predicate = cb.gt(from.<Number> get(key), number);
				} catch (NumberFormatException e) {
					logger.warn("GREATER操作，条件参数：" + key + "要求为数字，但实际不是数字![" + value + "]");
				}
			} else if (DwzCondition.Operation.GREATEROREQUALS.equals(condition.getOp())) {
				try {
					Number number = NumberUtils.createNumber(value);
					predicate = cb.ge(from.<Number> get(key), number);
				} catch (NumberFormatException e) {
					logger.warn("GREATEROREQUALS操作，条件参数：" + key + "要求为数字，但实际不是数字![" + value + "]");
				}
			} else if (DwzCondition.Operation.CONTAINS.equals(condition.getOp())) {
				predicate = cb.like(from.<String> get(key), cb.literal("%" + value + "%"));
			} else if (DwzCondition.Operation.STARTSWITH.equals(condition.getOp())) {
				predicate = cb.like(from.<String> get(key), cb.literal(value + "%"));
			} else if (DwzCondition.Operation.ENDSWITH.equals(condition.getOp())) {
				predicate = cb.like(from.<String> get(key), cb.literal("%" + value));
			}
		}

		return predicate;
	}

	@SuppressWarnings("rawtypes")
	private Class getFieldType(String propertyName) {
		try {
			Field field = domainClass.getDeclaredField(propertyName);
			return field.getType();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * 转换原生类型为其包装类型 boolean → Boolean char → Character byte → Byte short → Short
	 * int → Integer long → Long float → Float double → Double void → Void
	 * 
	 * @param primclass
	 *            原生类型
	 * @param value
	 *            值的字符串表示
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	private Object getPrimitiveObject(Class primclass, String value) {
		if (primclass.isPrimitive()) {
			if (boolean.class.isAssignableFrom(primclass)) {// boolean →Boolean
				return new Boolean(value);
			} else if (char.class.isAssignableFrom(primclass)) {// char
																// →Character
				return new Character(value.charAt(0));
			} else if (byte.class.isAssignableFrom(primclass)) {// byte →Byte
				return new Byte(value);
			} else if (short.class.isAssignableFrom(primclass)) {// short →Short
				return new Short(value);
			} else if (int.class.isAssignableFrom(primclass)) {// int →Integer
				return new Integer(value);
			} else if (long.class.isAssignableFrom(primclass)) {// long →Long
				return new Long(value);
			} else if (float.class.isAssignableFrom(primclass)) {// float →Float
				return new Float(value);
			} else if (double.class.isAssignableFrom(primclass)) {// double
																	// →Double
				return new Double(value);
			}
		} else {
			return null;
		}
		return null;
	}

}